查看原文
其他

今日份的面试题目:抽象工厂、Android常用布局、Java重入锁、守护线程、 SharedPreference存储大小

码仔 码个蛋 2019-08-28


码仔,今天就给大家带来了《每日一道面试题》的第八期:


01

谈谈对抽象工厂的理解 



抽象工厂模式,即Abstract Factory Pattern,提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类;具体的工厂负责实现具体的产品实例。

抽象工厂模式与工厂方法模式最大的区别:抽象工厂中每个工厂可以创建多种类的产品;而工厂方法每个工厂只能创建一类

主要作用


允许使用抽象的接口来创建一组相关产品,而不需要知道或关心实际生产出的具体产品是什么,这样就可以从具体产品中被解耦。


最后补充下三种工厂模式的区别

  • 简单工厂模式是由一个具体的类去创建其他类的实例,父类是相同的,父类是具体的。

  • 工厂方法模式是有一个抽象的父类定义公共接口,子类负责生成具体的对象,这样做的目的是将类的实例化操作延迟到子类中完成。

  • 抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无须指定他们具体的类。它针对的是有多个产品的等级结构。而工厂方法模式针对的是一个产品的等级结构。




02

请简述Android五种布局


最常用的布局方式为Absolute Layout、Relative Layout、Linear Layout、FrameLayout、TableLayout。其中Linear Layout和Relative Layout是最常用的方式,他们可以通过在xml配置文件或者代码中进行布局。

1、Frame Layout是最简单的布局方式,放置的控件都只能罗列到左上角,控件会有重叠,不能进行复杂的布局。

2、Linear Layout可以通过orientation属性设置线性排列的方向是垂直还是纵向的,每行或每列只有一个元素,可以进行复杂的布局。

3、Absolute Layout可以让子元素指定准确的x、y坐标值,并显示在屏幕上。Absolute Layout没有页边框,允许元素之间相互重叠。它是绝对坐标,所以在实际中不提倡使用。

4、Relative Layout允许子元素制定他们相对于其他元素或父元素的位置(通过ID制定)。因此,你可以以右对齐,或上下,或置于屏幕中央的形式来排列两个元素。元素按顺序排列,因此如果第一个元素在屏幕的中央,那么相对于这个元素的其他元素将以屏幕中央的相对位置来排列。这个是相对于Absolute Layout的,采用相对坐标,所以在实际中比较常用。

5、Table Layout将以子元素的位置分配到行或列。一个Table Layout由许多的Table Row组成,每个Table Row都会定义一个row。Table Layout容器不会显示row、column或者cell的边线框。每个row拥有0个或多个的cell; 和html中的table差不多。在实际中也经常使用。




03

谈谈对重入锁的理解 


重入锁

(1)重进入:1.定义:重进入是指任意线程在获取到锁之后,再次获取该锁而不会被该锁所阻塞。关联一个线程持有者+计数器,重入意味着锁操作的颗粒度为“线程”。

2.需要解决两个问题:线程再次获取锁:锁需要识别获取锁的现场是否为当前占据锁的线程,如果是,则再次成功获取;锁的最终释放:线程重复n次获取锁,随后在第n次释放该锁后,其他线程能够获取该锁。要求对锁对于获取进行次数的自增,计数器对当前锁被重复获取的次数进行统计,当锁被释放的时候,计数器自减,当计数器值为0时,表示锁成功释放。

3.重入锁实现重入性:每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁

(2)ReentrantLock是的非公平类中通过组合自定义同步器来实现锁的获取与释放。

1 /**
2 * Sync中的nonfairTryAcquire()方法实现
3 * 这个跟公平类中的实现主要区别在于不会判断当前线程是否是等待时间最长的线程
4 **/

5 final boolean nonfairTryAcquire(int acquires) {
6 final Thread current = Thread.currentThread();
7 int c = getState();
8 if (c == 0) {
9 // 跟FairSync中的主要区别,不会判断hasQueuedPredecessors()
10 if (compareAndSetState(0, acquires)) {
11 setExclusiveOwnerThread(current);
12 return true;
13 }
14 }
15 else if (current == getExclusiveOwnerThread()) {
16 int nextc = c + acquires;
17 if (nextc < 0) // overflow
18 throw new Error("Maximum lock count exceeded");
19 setState(nextc);
20 return true;
21 }
22 return false;
23 }


nonfairTryAcquire()方法中,增加了再次获取同步状态的处理逻辑,通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。成功获取锁的现场再次获取锁,只是增加了同步状态值,要求ReentrantLock在释放同步状态时减少同步状态值。


1 /**
2 * Sync中tryRelease()
3 **/

4 protected final boolean tryRelease(int releases) {
5 // 修改当前锁的状态
6 // 如果一个线程递归获取了该锁(也就是state != 1), 那么c可能不等0
7 // 如果没有线程递归获取该锁,则c == 0
8 int c = getState() - releases;
9
10 // 如果锁的占有线程不等于当前正在执行释放操作的线程,则抛出异常
11 if (Thread.currentThread() != getExclusiveOwnerThread())
12 throw new IllegalMonitorStateException();
13 boolean free = false;
14 // c == 0,表示当前线程释放锁成功,同时表示递归获取了该锁的线程已经执行完毕
15 // 则设置当前锁状态为free,同时设置锁的当前线程为null,可以让其他线程来获取
16 // 同时也说明,如果c != 0,则表示线程递归占用了锁资源,
17 // 所以锁的当前占用线程依然是当前释放锁的线程(实际没有释放)
18 if (c == 0) {
19 free = true;
20 setExclusiveOwnerThread(null);
21 }
22 // 重新设置锁的占有数
23 setState(c);
24 return free;
25 }


如果该锁被获取n次,则前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才返回true,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。对于公平锁而言


1 /**
2 * FairSync中tryAcquire()的实现
3 * 返回
4 * true: 获取锁成功
5 * false: 获取锁不成功
6 **/

7 protected final boolean tryAcquire(int acquires) {
8 // 获取当前线程
9 final Thread current = Thread.currentThread();
10 // 获取锁资源的状态
11 // 0: 说明当前锁可立即获取,在此种状态下(又是公平锁)
12 // >0并且当前线程与持有锁资源的线程是同一个线程则state + 1并返回true
13 // >0并且占有锁资源的不是当前线程,则返回false表示获取不成功
14 int c = getState();
15 if (c == 0) {
16 // 在锁可以立即获取的情况下
17 // 首先判断线程是否是刚刚释放锁资源的头节点的下一个节点(线程的等待先后顺序)
18 // 如果是等待时间最长的才会马上获取到锁资源,否则不会(这也是公平与不公平的主要区别所在)
19 if (!hasQueuedPredecessors() &&
20 compareAndSetState(0, acquires)) {
21 setExclusiveOwnerThread(current);
22 return true;
23 }
24 }
25 else if (current == getExclusiveOwnerThread()) { //线程可以递归获取锁
26 int nextc = c + acquires;
27 // 超过int上限值抛出错误
28 if (nextc < 0)
29 throw new Error("Maximum lock count exceeded");
30 setState(nextc);
31 return true;
32 }
33 return false;
34 }


与非公平唯一的区别是判断条件中多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回了true,则表示有线程比当前线程更早地请求获取锁,所以需要等待前驱线程获取并释放锁后才能继续获取该锁。 但是非公平锁是默认实现:非公平性锁可能使线程“饥饿”,但是极少的线程切换,可以保证其更大的吞吐量。而公平性锁,保证了锁的获取按照FIFO原则,代价是进行大量的线程切换。

(3)synchronized可重入性 同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入



04

守护线程与阻塞线程的四种情况 


守护线程 Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)

用户线程即运行在前台的线程,而守护线程是运行在后台的线程。 守护线程作用是为其他前台线程的运行提供便利服务,而且仅在普通、非守护线程仍然运行时才需要,比如垃圾回收线程就是一个守护线程。当VM检测仅剩一个守护线程,而用户线程都已经退出运行时,VM就会退出,因为没有如果没有了被守护这,也就没有继续运行程序的必要了。如果有非守护线程仍然存活,VM就不会退出。

守护线程并非只有虚拟机内部提供,用户在编写程序时也可以自己设置守护线程。用户可以用Thread的setDaemon(true)方法设置当前线程为守护线程。

虽然守护线程可能非常有用,但必须小心确保其他所有非守护线程消亡时,不会由于它的终止而产生任何危害。因为你不可能知道在所有的用户线程退出运行前,守护线程是否已经完成了预期的服务任务。一旦所有的用户线程退出了,虚拟机也就退出运行了。 因此,不要在守护线程中执行业务逻辑操作(比如对数据的读写等)。

另外有几点需要注意:

1、setDaemon(true)必须在调用线程的start()方法之前设置,否则会跑出IllegalThreadStateException异常。

2、在守护线程中产生的新线程也是守护线程。 3、 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。

线程阻塞 线程可以阻塞于四种状态:

1、当线程执行Thread.sleep()时,它一直阻塞到指定的毫秒时间之后,或者阻塞被另一个线程打断;

2、当线程碰到一条wait()语句时,它会一直阻塞到接到通知(notify())、被中断或经过了指定毫秒时间为止(若制定了超时值的话)

3、线程阻塞与不同I/O的方式有多种。常见的一种方式是InputStream的read()方法,该方法一直阻塞到从流中读取一个字节的数据为止,它可以无限阻塞,因此不能指定超时时间;

4、线程也可以阻塞等待获取某个对象锁的排他性访问权限(即等待获得synchronized语句必须的锁时阻塞)。

注意,并非所有的阻塞状态都是可中断的,以上阻塞状态的前两种可以被中断,后两种不会对中断做出反应



05

SharedPreference可以存多少数据 


Android中 SP 的底层是由Xml来实现的,操作SP的过程就是Xml的序列化和解析的过程。Xml是存储在磁盘上的,因此当我们频繁进行SP操作时,就是频繁进行序列化与解析,这就频繁进行I/O的操作,所以肯定会导致性能消耗。同时序列化Xml是就是将内存中的数据写到Xml文件中,由于DVM 的内存是很有限的,因此单个SP文件不建议太大,具体多大是没有一个具体的要求的,但是我们知道DVM 堆内存也就是16M,因此数据大小肯定不能超过这个数字的。其实 SP 设置的目的就是为了保存用户的偏好和配置信息的,因此不要保存太多的数据。


06

结束语 


如果你有好的答案可以提交至:

https://github.com/codeegginterviewgroup/CodeEggDailyInterview



往期文章:


专属社群:

《这件事情,我终于想明白了》 


今日问题:

大家想看什么类型的文章?




    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存